#include "BBS2chProxyKeyManager.h"
#include "BBS2chProxyAuth.h"
#include "BBS2chProxyConnection.h"
#include "parson/parson.h"
#include <stdlib.h>
#include <time.h>
#include <string.h>
#ifdef _WIN32
#define localtime_r(a, b) localtime_s(b, a)
#endif

extern double getCurrentTime(void);
extern void log_printf(int level, const char *format ...);

static void replaceAll(std::string &input, const std::string &oldValue, const std::string &newValue)
{
	if (!oldValue.empty()) {
		size_t pos = 0;
		while ((pos = input.find(oldValue, pos)) != std::string::npos) {
			input.replace(pos, oldValue.size(), newValue);
			pos += newValue.size();
		}
	}
}

static std::string escapeForHTML(const std::string &input)
{
	std::string out = input;
	replaceAll(out, "&", "&amp;");
	replaceAll(out, "<", "&lt;");
	replaceAll(out, ">", "&gt;");
	replaceAll(out, "\"", "&quot;");
	replaceAll(out, "'", "&#39;");
	return out;
}

const std::string BBS2chProxyKeyManager::_emptyKey = "00000000-0000-0000-0000-000000000000";

const std::string& BBS2chProxyKeyManager::getKey()
{
	if (_lastKey.empty())
		return _emptyKey;
	return _lastKey;
}

const std::string& BBS2chProxyKeyManager::getKey(const std::string &userAgent)
{
	pthread_mutex_lock(&_mutex);
	std::map<std::string, std::string>::iterator it = _keys.find(userAgent);
	if (it != _keys.end()) {
		const std::string &key = it->second;
		pthread_mutex_unlock(&_mutex);
		return key;
	}
	pthread_mutex_unlock(&_mutex);
	return _emptyKey;
}

void BBS2chProxyKeyManager::setKey(const std::string &key, const std::string &oldKey, const std::string &userAgent, int reason)
{
	/* do nothing when all-zero key is given - is this safe for all cases? */
	if (key == _emptyKey) {
		return;
	}
	pthread_mutex_lock(&_mutex);
	if (key.empty()) {
		if ((reason >= 3320 && reason <= 3324) || (reason >= 3390 && reason <= 3392)) {
			_expiredKeys.insert(oldKey);
		}
		_keys.erase(userAgent);
		log_printf(1, "Reset MonaKey for %s\n", userAgent.c_str());
	}
	else {
		_keys[userAgent] = key;
		_keyIssueTimes[key] = getCurrentTime();
		saveKeys();
		log_printf(1, "Updated MonaKey for %s: %s\n", userAgent.c_str(), key.c_str());
	}
	_lastKey = key;
	pthread_mutex_unlock(&_mutex);
}

bool BBS2chProxyKeyManager::isExpired(const std::string &key)
{
	return _expiredKeys.count(key) != 0;
}

double BBS2chProxyKeyManager::secondsToWaitBeforePosting(const std::string &key)
{
	double seconds = 0.0;
	pthread_mutex_lock(&_mutex);
	std::map<std::string, double>::iterator it = _keyIssueTimes.find(key);
	if (it != _keyIssueTimes.end()) {
		seconds = 4.0 - (getCurrentTime() - it->second);
	}
	pthread_mutex_unlock(&_mutex);
	return seconds;
}

void BBS2chProxyKeyManager::setStorage(const char *jsonPath)
{
	_storagePath = jsonPath;
}

/* should be called once on main thread */
int BBS2chProxyKeyManager::loadKeys()
{
	if (_storagePath.empty()) return 0;
	JSON_Value *json = json_parse_file(_storagePath.c_str());
	if (!json) {
		FILE *fp = fopen(_storagePath.c_str(), "rb");
		if (fp) {
			if (fseek(fp, 0, SEEK_END) || ftell(fp)) {
				fclose(fp);
				return -1;
			}
			fclose(fp);
		}
		return 0;
	}
	if (json_type(json) != JSONObject) {
		json_value_free(json);
		return 0;
	}
	JSON_Object *root = json_object(json);
	JSON_Object *keyStore = json_object_get_object(root, "MonaKeys");
	int numKeys = 0;
	if (keyStore) {
		double latest = -1;
		size_t length = json_object_get_count(keyStore);
		for (size_t i=0; i<length; i++) {
			const char *userAgent = json_object_get_name(keyStore, i);
			JSON_Value *entry = json_object_get_value_at(keyStore, i);
			if (json_type(entry) != JSONObject) continue;
			const char *key = json_object_get_string(json_object(entry), "key");
			double issued_on = json_object_get_number(json_object(entry), "issued_on");
			if (key) {
				numKeys++;
				_keys[userAgent] = key;
				if (issued_on > 0) {
					_keyIssueTimes[key] = issued_on;
				}
				if (issued_on > latest) {
					_lastKey = key;
				}
			}
		}
	}
	JSON_Object *cookies = json_object_get_object(root, "Cookies");
	int numCookies = 0;
	if (cookies) {
		size_t length = json_object_get_count(cookies);
		for (size_t i=0; i<length; i++) {
			std::string userAgent = json_object_get_name(cookies, i);
			JSON_Value *array = json_object_get_value_at(cookies, i);
			if (json_type(array) != JSONArray) continue;
			size_t numEntries = json_array_get_count(json_array(array));
			for (size_t j=0; j<numEntries; j++) {
				JSON_Value *entry = json_array_get_value(json_array(array), j);
				if (json_type(entry) != JSONObject) continue;
				JSON_Object *entryObj = json_object(entry);
				Cookie cookie;
				const char *str = json_object_get_string(entryObj, "name");
				if (str) cookie.name = str;
				str = json_object_get_string(entryObj, "value");
				if (str) cookie.value = str;
				str = json_object_get_string(entryObj, "domain");
				if (str) cookie.domain = str;
				cookie.includeSubdomains = json_object_get_boolean(entryObj, "includeSubdomains");
				str = json_object_get_string(entryObj, "path");
				if (str) cookie.path = str;
				cookie.secure = json_object_get_boolean(entryObj, "secure");
				str = json_object_get_string(entryObj, "expires");
				if (str) cookie.expires = str;
				_cookies[userAgent].set(cookie);
				numCookies++;
			}
		}
	}
	json_value_free(json);
	return numCookies;
}

/* this private function is called inside setKey(), and should not be called from other places */
bool BBS2chProxyKeyManager::saveKeys()
{
	if (_storagePath.empty()) return false;
	JSON_Value *json = json_parse_file(_storagePath.c_str());
	if (!json) {
		json = json_value_init_object();
	}
	else if (json_type(json) != JSONObject) {
		log_printf(0, "The file %s looks like JSON but unusable as a MonaKey storage.\n", _storagePath.c_str());
		json_value_free(json);
		return false;
	}
	JSON_Object *root = json_object(json);
	JSON_Value *object = json_value_init_object();
	json_object_set_value(root, "MonaKeys", object);
	JSON_Object *keyStore = json_object(object);
	for (std::map<std::string, std::string>::iterator it = _keys.begin(); it != _keys.end(); it++) {
		const std::string &userAgent = it->first;
		const std::string &key = it->second;
		double issued_on = 0;
		std::map<std::string, double>::iterator it2 = _keyIssueTimes.find(key);
		if (it2 != _keyIssueTimes.end()) {
			issued_on = it2->second;
		}
		JSON_Value *entry = json_value_init_object();
		json_object_set_string(json_object(entry), "key", key.c_str());
		if (issued_on > 0) json_object_set_number(json_object(entry), "issued_on", issued_on);
		json_object_set_value(keyStore, userAgent.c_str(), entry);
	}
	bool ret = json_serialize_to_file_pretty(json, _storagePath.c_str()) == JSONSuccess;
	if (!ret) {
		log_printf(0, "Error while writing MonaKeys to %s\n", _storagePath.c_str());
	}
	json_value_free(json);
	return ret;
}

BBS2chProxyKeyManager::CookieJar& BBS2chProxyKeyManager::getCookieJar(const std::string &userAgent, bool syncCookie)
{
	pthread_mutex_lock(&_mutex);
	BBS2chProxyKeyManager::CookieJar& jar = _cookies[userAgent];
	pthread_mutex_unlock(&_mutex);
	if (syncCookie) BBS2chProxyConnection::cookieAuthManager.syncCookies(jar);
	return jar;
}

bool BBS2chProxyKeyManager::flushCookies()
{
	if (_storagePath.empty()) return false;
	JSON_Value *json = json_parse_file(_storagePath.c_str());
	if (!json) {
		json = json_value_init_object();
	}
	else if (json_type(json) != JSONObject) {
		log_printf(0, "The file %s looks like JSON but unusable as a cookie storage.\n", _storagePath.c_str());
		json_value_free(json);
		return false;
	}
	JSON_Object *root = json_object(json);
	JSON_Value *object = json_value_init_object();
	json_object_set_value(root, "Cookies", object);
	pthread_mutex_lock(&_mutex);
	for (std::map<std::string, CookieJar>::iterator it = _cookies.begin(); it != _cookies.end(); ++it) {
		it->second.lock();
		JSON_Value *array = (JSON_Value *)it->second.jsonValue();
		if (array && json_array_get_count(json_array(array))) {
			json_object_set_value(json_object(object), it->first.c_str(), array);
		}
		it->second.unlock();
	}
	pthread_mutex_unlock(&_mutex);
	bool ret = json_serialize_to_file_pretty(json, _storagePath.c_str()) == JSONSuccess;
	if (!ret) {
		log_printf(0, "Error while writing cookies to %s\n", _storagePath.c_str());
	}
	json_value_free(json);
	return ret;
}

std::string BBS2chProxyKeyManager::cookieManagerHTML()
{
	char dateStr[256];
	std::string html;
	html += "<!DOCTYPE html><html lang=\"ja\"><head>";
	html += "<title>proxy2ch: クッキー管理</title><style>";
	html += ".table-container { overflow-x : auto; }";
	html += "table { table-layout: fixed; border-collapse: collapse; width: 100%; min-width: 100ch; }";
	html += "th { font-weight: normal; }";
	html += "th span { font-weight: bold; }";
	html += "tr { border-top: 1px solid #ddd; }";
	html += "th:not(:last-child), td:not(:last-child) { border-right: 1px solid #ddd; }";
	html += "thead tr, tbody tr:nth-child(even) { background-color: #eee; }";
	html += "td div { box-sizing: content-box; white-space: normal; overflow-x: hidden; word-break: break-all; max-height: 4.2em; padding: 0 5px }";
	html += ".expire { word-break: normal; }";
	html += ".actions { text-align: center; }";
	html += ".col_name, .col_doma { width: 15ch; }";
	html += ".col_expi { width: 20ch; }";
	html += ".col_butt { width: 10ch; }";
	html += "</style><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">";
	html += "<meta http-equiv=\"content-type\" content=\"text/html; charset=UTF-8\"></head><body>";
	bool isEmpty = true;
	pthread_mutex_lock(&_mutex);
	for (std::map<std::string, CookieJar>::iterator it = _cookies.begin(); it != _cookies.end(); ++it) {
		it->second.lock();
		std::vector<Cookie>& cookies = it->second.getList();
		if (cookies.empty()) {
			it->second.unlock();
			continue;
		}
		std::string key = escapeForHTML(it->first);
		html += "<h3>";
		html += key;
		html += " のクッキー</h3><div class=\"table-container\"><table><thead><tr>";
		html += "<th class=\"col_name\"><span>名前</span></th>";
		html += "<th class=\"col_valu\"><span>値</span></th>";
		html += "<th class=\"col_doma\"><span>ドメイン</span></th>";
		html += "<th class=\"col_expi\"><span>期限</span></th>";
		html += "<th class=\"col_butt\"></th></tr></thead><tbody>";
		for (std::vector<Cookie>::iterator it2 = cookies.begin(); it2 != cookies.end(); ++it2) {
			time_t expires = (time_t)strtoll(it2->expires.c_str(), NULL, 10);
			if (expires != 0) {
				struct tm expires_tm = {};
				localtime_r(&expires, &expires_tm);
				strftime(dateStr, 256, "%Y/%m/%d %H:%M:%S", &expires_tm);
			} else {
				strcpy(dateStr, "セッション");
			}
			std::string name = escapeForHTML(it2->name);
			std::string value = escapeForHTML(it2->value);
			std::string domain = escapeForHTML(it2->domain);
			std::string path = escapeForHTML(it2->path);
			html += "<tr><td><div>";
			html += name;
			html += "</div></td><td><div>";
			html += value;
			html += "</div></td><td><div>";
			html += domain;
			html += "</div></td><td><div class=\"expire\">";
			html += dateStr;
			html += "</div></td><td class=\"actions\"><form action=\"cookies\" method=\"POST\">";
			html += "<input type=\"hidden\" name=\"action\" value=\"delete\">";
			html += "<input type=\"hidden\" name=\"name\" value=\"";
			html += name;
			html += "\"><input type=\"hidden\" name=\"domain\" value=\"";
			html += domain;
			html += "\"><input type=\"hidden\" name=\"path\" value=\"";
			html += path;
			html += "\"><input type=\"hidden\" name=\"key\" value=\"";
			html += key;
			html += "\"><button>削除</button></form></td></tr>";
		}
		it->second.unlock();
		html += "</tbody></table></div>";
		isEmpty = false;
	}
	pthread_mutex_unlock(&_mutex);
	if (isEmpty) html += "proxy2chが管理しているクッキーはありません。";
	html += "</body></html>";
	return html;
}

BBS2chProxyKeyManager::Cookie::Cookie(const std::string &valueInNetscapeFormat)
{
	std::vector<std::string> list;
	size_t offset = 0;
	while (1) {
		size_t pos = valueInNetscapeFormat.find('\t', offset);
		if (pos == std::string::npos) {
			list.push_back(valueInNetscapeFormat.substr(offset));
			break;
		}
		list.push_back(valueInNetscapeFormat.substr(offset, pos - offset));
		offset = pos + 1;
	}
	if (list.size() == 7) {
		name = list[5];
		value = list[6];
		domain = list[0];
		includeSubdomains = list[1] == "TRUE" ? true : false;
		path = list[2];
		secure = list[3] == "TRUE" ? true : false;
		expires = list[4];
	}
}

std::string BBS2chProxyKeyManager::Cookie::valueInNetscapeFormat()
{
	std::string ret = domain;
	ret += '\t';
	ret += includeSubdomains ? "TRUE" : "FALSE";
	ret += '\t';
	ret += path;
	ret += '\t';
	ret += secure ? "TRUE" : "FALSE";
	ret += '\t';
	ret += expires;
	ret += '\t';
	ret += name;
	ret += '\t';
	ret += value;
	return ret;
}

bool BBS2chProxyKeyManager::Cookie::isExpired()
{
	return expires != "0" && strtoull(expires.c_str(), NULL, 10) < time(NULL);
}

bool BBS2chProxyKeyManager::Cookie::isSameAs(Cookie &cookie)
{
	return (name == cookie.name) && (domain == cookie.domain) && (path == cookie.path);
}

void *BBS2chProxyKeyManager::Cookie::jsonValue()
{
	if (isExpired() || expires == "0") return NULL;
	JSON_Value *entry = json_value_init_object();
	json_object_set_string(json_object(entry), "name", name.c_str());
	json_object_set_string(json_object(entry), "value", value.c_str());
	json_object_set_string(json_object(entry), "domain", domain.c_str());
	json_object_set_boolean(json_object(entry), "includeSubdomains", includeSubdomains);
	json_object_set_string(json_object(entry), "path", path.c_str());
	json_object_set_boolean(json_object(entry), "secure", secure);
	json_object_set_string(json_object(entry), "expires", expires.c_str());
	return entry;
}

std::vector<BBS2chProxyKeyManager::Cookie>& BBS2chProxyKeyManager::CookieJar::getList()
{
	return _cookies;
}

void BBS2chProxyKeyManager::CookieJar::set(Cookie &cookie)
{
	if (cookie.name.empty() || cookie.domain.empty() || cookie.path.empty()) return;
	for (std::vector<Cookie>::iterator it = _cookies.begin(); it != _cookies.end(); ++it) {
		if (it->isSameAs(cookie)) {
			if (cookie.isExpired()) _cookies.erase(it);
			else *it = cookie;
			return;
		}
	}
	if (cookie.isExpired()) return;
	_cookies.push_back(cookie);
}

void BBS2chProxyKeyManager::CookieJar::clear()
{
	_cookies.clear();
}

void BBS2chProxyKeyManager::CookieJar::lock()
{
	pthread_mutex_lock(&_mutex);
}

void BBS2chProxyKeyManager::CookieJar::unlock()
{
	pthread_mutex_unlock(&_mutex);
}

void *BBS2chProxyKeyManager::CookieJar::jsonValue()
{
	JSON_Value *array = json_value_init_array();
	for (std::vector<Cookie>::iterator it = _cookies.begin(); it != _cookies.end(); ++it) {
		JSON_Value *entry = (JSON_Value *)it->jsonValue();
		if (entry) json_array_append_value(json_array(array), entry);
	}
	return array;
}
